use {
    crate::{identifier::Identifier, schema},
    std::{
        collections::BTreeMap,
        fmt::{self, Write},
        path::PathBuf,
    },
};

// The string to be used for each indentation level.
const INDENTATION: &str = "  ";

// This is the full list of TypeScript keywords, derived from:
//   https://github.com/microsoft/TypeScript/blob/2161e1852f4f627bbc7571c6b7284f419ec524c9
//   /src/compiler/types.ts#L113-L194
const TYPESCRIPT_KEYWORDS: &[&str] = &[
    "abstract",
    "any",
    "as",
    "assert",
    "asserts",
    "async",
    "await",
    "bigint",
    "boolean",
    "break",
    "case",
    "catch",
    "class",
    "const",
    "constructor",
    "continue",
    "debugger",
    "declare",
    "default",
    "delete",
    "do",
    "else",
    "enum",
    "export",
    "extends",
    "false",
    "finally",
    "for",
    "from",
    "function",
    "get",
    "global",
    "if",
    "implements",
    "import",
    "in",
    "infer",
    "instanceof",
    "interface",
    "intrinsic",
    "is",
    "keyof",
    "let",
    "module",
    "namespace",
    "never",
    "new",
    "null",
    "number",
    "object",
    "of",
    "override",
    "package",
    "private",
    "protected",
    "public",
    "readonly",
    "require",
    "return",
    "set",
    "static",
    "string",
    "super",
    "switch",
    "symbol",
    "this",
    "throw",
    "true",
    "try",
    "type",
    "typeof",
    "undefined",
    "unique",
    "unknown",
    "var",
    "void",
    "while",
    "with",
    "yield",
];

// This struct represents a tree of schemas organized in a module hierarchy.
#[derive(Clone, Debug)]
struct Module {
    children: BTreeMap<Identifier, Module>,
    schema: schema::Schema,
}

// This enum represents a case convention for the `write_identifier` function below.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum CaseConvention {
    Camel,
    Pascal,
}

use CaseConvention::{Camel, Pascal};

// This enum is used to distinguish between the ingress and egress versions of a type.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum Direction {
    Atlas,
    In,
    Out,
}

use Direction::{Atlas, In, Out};

// Generate TypeScript code from a schema and its transitive dependencies.
#[allow(clippy::too_many_lines)]
pub fn generate(
    typical_version: &str,
    schemas: &BTreeMap<schema::Namespace, (schema::Schema, PathBuf, String)>,
) -> String {
    // Construct a tree of modules and schemas. We start with an empty tree.
    let mut tree = Module {
        children: BTreeMap::new(),
        schema: schema::Schema {
            comment: vec![],
            imports: BTreeMap::new(),
            declarations: vec![],
        },
    };

    // Populate the tree with all the schemas.
    for (namespace, (schema, _, _)) in schemas {
        insert_schema(&mut tree, namespace, schema);
    }

    // Write the code.
    let mut buffer = String::new();

    if !tree.children.is_empty() || !tree.schema.declarations.is_empty() {
        // The `unwrap` is safe because the `std::fmt::Write` impl for `String` is infallible.
        writeln!(
            &mut buffer,
            "\
// This file was automatically generated by Typical {typical_version}.
// Visit https://github.com/stepchowfun/typical for more information.

/* eslint-disable */

export type Deserializable =
  | ArrayBuffer
  | DataView
  | Int8Array
  | Uint8Array
  | Uint8ClampedArray
  | Int16Array
  | Uint16Array
  | Int32Array
  | Uint32Array
  | Float32Array
  | Float64Array
  | BigInt64Array
  | BigUint64Array;

export function unreachable(x: never): never {{
  return x;
}}

function zigzagEncode(value: bigint): bigint {{
  const twice = value << 1n;
  return value < 0n ? -1n - twice : twice;
}}

function zigzagDecode(value: bigint): bigint {{
  const half = (value + 1n) >> 1n;
  return (value & 1n) === 0n ? half : -half;
}}

function varintSizeFromValue(value: bigint): number {{
  if (value < 128n) {{
    return 1;
  }}

  if (value < 16_512n) {{
    return 2;
  }}

  if (value < 2_113_664n) {{
    return 3;
  }}

  if (value < 270_549_120n) {{
    return 4;
  }}

  if (value < 34_630_287_488n) {{
    return 5;
  }}

  if (value < 4_432_676_798_592n) {{
    return 6;
  }}

  if (value < 567_382_630_219_904n) {{
    return 7;
  }}

  if (value < 72_624_976_668_147_840n) {{
    return 8;
  }}

  return 9;
}}

function varintSizeFromFirstByte(firstByte: number): number {{
  let trailingZeros = 0;

  while (trailingZeros < 8 && (firstByte & 1) !== 1) {{
    trailingZeros += 1;
    firstByte >>= 1;
  }}

  return trailingZeros + 1;
}}

function serializeVarint(
  dataView: DataView,
  offset: number,
  value: bigint,
): number {{
  if (value < 128n) {{
    dataView.setUint8(offset, Number(value << 1n) | 0b0000_0001);
    return offset + 1;
  }}

  if (value < 16_512n) {{
    value -= 128n;
    dataView.setUint8(offset, Number((value << 2n) % 256n) | 0b0000_0010);
    dataView.setUint8(offset + 1, Number(value >> 6n));
    return offset + 2;
  }}

  if (value < 2_113_664n) {{
    value -= 16_512n;
    dataView.setUint8(offset, Number((value << 3n) % 256n) | 0b0000_0100);
    dataView.setUint16(offset + 1, Number((value >> 5n) % 65_536n), true);
    return offset + 3;
  }}

  if (value < 270_549_120n) {{
    value -= 2_113_664n;
    dataView.setUint8(offset, Number((value << 4n) % 256n) | 0b0000_1000);
    dataView.setUint8(offset + 1, Number((value >> 4n) % 256n));
    dataView.setUint16(offset + 2, Number((value >> 12n) % 65_536n), true);
    return offset + 4;
  }}

  if (value < 34_630_287_488n) {{
    value -= 270_549_120n;
    dataView.setUint8(offset, Number((value << 5n) % 256n) | 0b0001_0000);
    dataView.setUint32(
      offset + 1,
      Number((value >> 3n) % 4_294_967_296n),
      true,
    );
    return offset + 5;
  }}

  if (value < 4_432_676_798_592n) {{
    value -= 34_630_287_488n;
    dataView.setUint8(offset, Number((value << 6n) % 256n) | 0b0010_0000);
    dataView.setUint8(offset + 1, Number((value >> 2n) % 256n));
    dataView.setUint32(
      offset + 2,
      Number((value >> 10n) % 4_294_967_296n),
      true,
    );
    return offset + 6;
  }}

  if (value < 567_382_630_219_904n) {{
    value -= 4_432_676_798_592n;
    dataView.setUint8(offset, Number((value << 7n) % 256n) | 0b0100_0000);
    dataView.setUint16(offset + 1, Number((value >> 1n) % 65_536n), true);
    dataView.setUint32(
      offset + 3,
      Number((value >> 17n) % 4_294_967_296n),
      true,
    );
    return offset + 7;
  }}

  if (value < 72_624_976_668_147_840n) {{
    value -= 567_382_630_219_904n;
    dataView.setUint8(offset, 0b1000_0000);
    dataView.setUint8(offset + 1, Number(value % 256n));
    dataView.setUint16(offset + 2, Number((value >> 8n) % 65_536n), true);
    dataView.setUint32(
      offset + 4,
      Number((value >> 24n) % 4_294_967_296n),
      true,
    );
    return offset + 8;
  }}

  value -= 72_624_976_668_147_840n;
  dataView.setUint8(offset, 0b0000_0000);
  dataView.setBigUint64(offset + 1, value, true);
  return offset + 9;
}}

function deserializeVarint(
  dataView: DataView,
  offset: number,
): [number, bigint] {{
  const firstByte = dataView.getUint8(offset);
  const sizeMinusOne = varintSizeFromFirstByte(firstByte) - 1;

  const offsetPlusOne = offset + 1;
  dataView64.setBigUint64(0, 0n, true);
  for (let i = 0; i < sizeMinusOne; i += 1) {{
    dataView64.setUint8(i, dataView.getUint8(offsetPlusOne + i));
  }}
  const remainingBytesValue = dataView64.getBigUint64(0, true);

  switch (sizeMinusOne) {{
    case 0:
      return [offset + 1, BigInt(firstByte >> 1)];
    case 1:
      return [
        offset + 2,
        128n + BigInt(firstByte >> 2) + (remainingBytesValue << 6n),
      ];
    case 2:
      return [
        offset + 3,
        16_512n + BigInt(firstByte >> 3) + (remainingBytesValue << 5n),
      ];
    case 3:
      return [
        offset + 4,
        2_113_664n + BigInt(firstByte >> 4) + (remainingBytesValue << 4n),
      ];
    case 4:
      return [
        offset + 5,
        270_549_120n + BigInt(firstByte >> 5) + (remainingBytesValue << 3n),
      ];
    case 5:
      return [
        offset + 6,
        34_630_287_488n + BigInt(firstByte >> 6) + (remainingBytesValue << 2n),
      ];
    case 6:
      return [
        offset + 7,
        4_432_676_798_592n +
          BigInt(firstByte >> 7) +
          (remainingBytesValue << 1n),
      ];
    case 7:
      return [offset + 8, 567_382_630_219_904n + remainingBytesValue];
    default:
      return [
        offset + 9,
        (72_624_976_668_147_840n + remainingBytesValue) %
          18_446_744_073_709_551_616n,
      ];
  }}
}}

function fieldHeaderSize(
  index: bigint,
  payloadSize: number,
  integerEncoded: boolean,
): number {{
  switch (payloadSize) {{
    case 0:
      return varintSizeFromValue(index << 2n);
    case 8:
      return varintSizeFromValue((index << 2n) | 1n);
    default:
      if (integerEncoded) {{
        return varintSizeFromValue((index << 2n) | 2n);
      }}

      return (
        varintSizeFromValue((index << 2n) | 3n) +
        varintSizeFromValue(BigInt(payloadSize))
      );
  }}
}}

function serializeFieldHeader(
  dataView: DataView,
  offset: number,
  index: bigint,
  payloadSize: number,
  integerEncoded: boolean,
): number {{
  switch (payloadSize) {{
    case 0:
      return serializeVarint(dataView, offset, index << 2n);
    case 8:
      return serializeVarint(dataView, offset, (index << 2n) | 1n);
    default:
      if (integerEncoded) {{
        return serializeVarint(dataView, offset, (index << 2n) | 2n);
      }}

      offset = serializeVarint(dataView, offset, (index << 2n) | 3n);

      return serializeVarint(dataView, offset, BigInt(payloadSize));
  }}
}}

function deserializeFieldHeader(
  dataView: DataView,
  offset: number,
): [number, bigint, number] {{
  const [newOffset, tag] = deserializeVarint(dataView, offset);

  const index = tag >> 2n;

  switch (tag & 3n) {{
    case 0n:
      return [newOffset, index, 0];
    case 1n:
      return [newOffset, index, 8];
    case 2n:
      return [newOffset, index, varintSizeFromFirstByte(dataView.getUint8(newOffset))];
    default: {{
      const [newNewOffset, sizeValue] = deserializeVarint(dataView, newOffset);
      return [newNewOffset, index, Number(sizeValue)];
    }}
  }}
}}

const missingFieldsErrorMessage = 'Struct missing one or more required field(s).';
const dataView64 = new DataView(new ArrayBuffer(8));
const textEncoder = new TextEncoder();
const textDecoder = new TextDecoder();",
        )
        .unwrap();

        // The `unwrap` is safe because the `std::fmt::Write` impl for `String` is infallible.
        writeln!(&mut buffer).unwrap();

        // The `unwrap` is safe because the `std::fmt::Write` impl for `String` is infallible.
        write_module_contents(
            &mut buffer,
            0,
            &schema::Namespace { components: vec![] },
            &tree.children,
            &tree.schema,
        )
        .unwrap();
    }

    buffer
}

// Insert a schema into a module.
fn insert_schema(module: &mut Module, namespace: &schema::Namespace, schema: &schema::Schema) {
    let mut iter = namespace.components.iter();

    if let Some(head) = iter.next() {
        if let Some(child) = module.children.get_mut(head) {
            insert_schema(
                child,
                &schema::Namespace {
                    components: iter.cloned().collect(),
                },
                schema,
            );
        } else {
            let mut child = Module {
                children: BTreeMap::new(),
                schema: schema::Schema {
                    comment: vec![],
                    imports: BTreeMap::new(),
                    declarations: vec![],
                },
            };

            insert_schema(
                &mut child,
                &schema::Namespace {
                    components: iter.cloned().collect(),
                },
                schema,
            );

            module.children.insert(head.clone(), child);
        }
    } else {
        module.schema = schema.clone();
    }
}

// Write a module, including a trailing line break.
fn write_module<T: Write>(
    buffer: &mut T,
    indentation: usize,
    namespace: &schema::Namespace,
    name: &Identifier,
    module: &Module,
) -> Result<(), fmt::Error> {
    write_indentation(buffer, indentation)?;
    write!(buffer, "export namespace ")?;
    write_identifier(buffer, name, Pascal, None)?;
    writeln!(buffer, " {{")?;

    let mut new_namespace = namespace.clone();
    new_namespace.components.push(name.clone());

    write_module_contents(
        buffer,
        indentation + 1,
        &new_namespace,
        &module.children,
        &module.schema,
    )?;

    write_indentation(buffer, indentation)?;
    writeln!(buffer, "}}")?;

    Ok(())
}

// Write the contents of a module, including a trailing line break if there was anything to render.
fn write_module_contents<T: Write>(
    buffer: &mut T,
    indentation: usize,
    namespace: &schema::Namespace,
    children: &BTreeMap<Identifier, Module>,
    schema: &schema::Schema,
) -> Result<(), fmt::Error> {
    let schema_empty = schema.declarations.is_empty();

    for (i, (child_name, child)) in children.iter().enumerate() {
        write_module(buffer, indentation, namespace, child_name, child)?;

        if i < children.len() - 1 || !schema_empty {
            writeln!(buffer)?;
        }
    }

    write_schema(buffer, indentation, namespace, schema)?;

    Ok(())
}

// Write a schema, including a trailing line break if there was anything to render.
#[allow(clippy::too_many_lines)]
fn write_schema<T: Write>(
    buffer: &mut T,
    indentation: usize,
    namespace: &schema::Namespace,
    schema: &schema::Schema,
) -> Result<(), fmt::Error> {
    // Construct a map from import name to namespace.
    let mut imports = BTreeMap::new();
    for (name, import) in &schema.imports {
        // The `unwrap` is safe due to [ref:namespace_populated].
        imports.insert(name.clone(), import.namespace.clone().unwrap());
    }

    // Write the declarations.
    let mut iter = schema.declarations.iter().peekable();
    while let Some(declaration) = iter.next() {
        match &declaration.variant {
            schema::DeclarationVariant::Struct => {
                write_struct(
                    buffer,
                    indentation,
                    &imports,
                    namespace,
                    &declaration.name,
                    &declaration.fields,
                    Atlas,
                )?;

                writeln!(buffer)?;

                write_struct(
                    buffer,
                    indentation,
                    &imports,
                    namespace,
                    &declaration.name,
                    &declaration.fields,
                    Out,
                )?;

                writeln!(buffer)?;

                write_struct(
                    buffer,
                    indentation,
                    &imports,
                    namespace,
                    &declaration.name,
                    &declaration.fields,
                    In,
                )?;

                writeln!(buffer)?;

                write_indentation(buffer, indentation)?;
                write!(buffer, "export namespace ")?;
                write_identifier(buffer, &declaration.name, Pascal, None)?;
                writeln!(buffer, " {{")?;

                write_size_function(
                    buffer,
                    indentation + 1,
                    &declaration.name,
                    &declaration.fields,
                )?;

                writeln!(buffer)?;

                write_serialize_function(
                    buffer,
                    indentation + 1,
                    &declaration.variant,
                    &declaration.name,
                    &declaration.fields,
                )?;

                writeln!(buffer)?;

                write_deserialize_function(buffer, indentation + 1, &declaration.name)?;

                writeln!(buffer)?;

                write_indentation(buffer, indentation + 1)?;
                write!(buffer, "export function atlas(message: ")?;
                write_identifier(buffer, &declaration.name, Pascal, Some(Out))?;
                write!(buffer, "): ")?;
                write_identifier(buffer, &declaration.name, Pascal, Some(Atlas))?;
                writeln!(buffer, " {{")?;
                write_indentation(buffer, indentation + 2)?;
                writeln!(buffer, "let size = 0;")?;
                if !declaration.fields.is_empty() {
                    writeln!(buffer)?;
                    write_indentation(buffer, indentation + 2)?;
                    write!(buffer, "let ")?;
                    let mut first = true;
                    for field in &declaration.fields {
                        if first {
                            first = false;
                        } else {
                            write!(buffer, ", ")?;
                        }
                        write!(buffer, "$")?;
                        write_identifier(buffer, &field.name, Camel, None)?;
                    }
                    writeln!(buffer, ";")?;
                }
                for field in &declaration.fields {
                    writeln!(buffer)?;
                    write_indentation(buffer, indentation + 2)?;
                    writeln!(buffer, "{{")?;
                    write_indentation(buffer, indentation + 3)?;
                    writeln!(buffer, "let payloadAtlas;")?;
                    write_indentation(buffer, indentation + 3)?;
                    write!(buffer, "const payload = message.")?;
                    write_identifier(buffer, &field.name, Camel, None)?;
                    writeln!(buffer, ";")?;
                    let conditional_indentation = match field.rule {
                        schema::Rule::Asymmetric | schema::Rule::Required => indentation + 3,
                        schema::Rule::Optional => {
                            write_indentation(buffer, indentation + 3)?;
                            writeln!(buffer, "if (payload !== undefined) {{")?;
                            indentation + 4
                        }
                    };
                    write_atlas_calculation(
                        buffer,
                        conditional_indentation,
                        &imports,
                        namespace,
                        &field.r#type.variant,
                        true,
                    )?;
                    write_indentation(buffer, conditional_indentation)?;
                    write!(buffer, "$")?;
                    write_identifier(buffer, &field.name, Camel, None)?;
                    writeln!(buffer, " = payloadAtlas;")?;
                    write_indentation(buffer, conditional_indentation)?;
                    write!(buffer, "const payloadSize = ")?;
                    write_atlas_lookup(buffer, &field.r#type.variant)?;
                    writeln!(buffer, ";")?;
                    write_indentation(buffer, conditional_indentation)?;
                    writeln!(
                        buffer,
                        "size += fieldHeaderSize({}n, payloadSize, {}) + payloadSize;",
                        field.index,
                        integer_encoded(&field.r#type),
                    )?;
                    match field.rule {
                        schema::Rule::Asymmetric | schema::Rule::Required => {}
                        schema::Rule::Optional => {
                            write_indentation(buffer, indentation + 3)?;
                            writeln!(buffer, "}}")?;
                        }
                    }
                    write_indentation(buffer, indentation + 2)?;
                    writeln!(buffer, "}}")?;
                }
                writeln!(buffer)?;
                write_indentation(buffer, indentation + 2)?;
                writeln!(buffer, "return {{")?;
                write_indentation(buffer, indentation + 3)?;
                writeln!(buffer, "$size: size,")?;
                for field in &declaration.fields {
                    write_indentation(buffer, indentation + 3)?;
                    write_identifier(buffer, &field.name, Camel, None)?;
                    write!(buffer, ": $")?;
                    write_identifier(buffer, &field.name, Camel, None)?;
                    writeln!(buffer, ",")?;
                }
                write_indentation(buffer, indentation + 2)?;
                writeln!(buffer, "}};")?;
                write_indentation(buffer, indentation + 1)?;
                writeln!(buffer, "}}")?;

                writeln!(buffer)?;

                // This function is "unsafe" in the sense that it can throw an error if
                // `dataView` isn't big enough or if `atlas` has incorrect values.
                write_indentation(buffer, indentation + 1)?;
                writeln!(buffer, "export function serializeWithAtlasUnsafe(")?;
                write_indentation(buffer, indentation + 2)?;
                writeln!(buffer, "dataView: DataView,")?;
                write_indentation(buffer, indentation + 2)?;
                writeln!(buffer, "offset: number,")?;
                write_indentation(buffer, indentation + 2)?;
                write!(buffer, "message: ")?;
                write_identifier(buffer, &declaration.name, Pascal, Some(Out))?;
                writeln!(buffer, ",")?;
                write_indentation(buffer, indentation + 2)?;
                write!(buffer, "atlas: ")?;
                write_identifier(buffer, &declaration.name, Pascal, Some(Atlas))?;
                writeln!(buffer, ",")?;
                write_indentation(buffer, indentation + 1)?;
                writeln!(buffer, "): number {{")?;
                for field in &declaration.fields {
                    write_indentation(buffer, indentation + 2)?;
                    writeln!(buffer, "{{")?;
                    write_indentation(buffer, indentation + 3)?;
                    write!(buffer, "const payload = message.")?;
                    write_identifier(buffer, &field.name, Camel, None)?;
                    writeln!(buffer, ";")?;
                    write_indentation(buffer, indentation + 3)?;
                    write!(buffer, "const payloadAtlas = atlas.")?;
                    write_identifier(buffer, &field.name, Camel, None)?;
                    writeln!(buffer, ";")?;
                    let conditional_indentation = match field.rule {
                        schema::Rule::Asymmetric | schema::Rule::Required => indentation + 3,
                        schema::Rule::Optional => {
                            write_indentation(buffer, indentation + 3)?;
                            writeln!(
                                buffer,
                                "if (payload !== undefined && payloadAtlas !== undefined) {{",
                            )?;
                            indentation + 4
                        }
                    };
                    write_indentation(buffer, conditional_indentation)?;
                    write!(buffer, "const payloadSize = ")?;
                    write_atlas_lookup(buffer, &field.r#type.variant)?;
                    writeln!(buffer, ";")?;
                    write_indentation(buffer, conditional_indentation)?;
                    writeln!(
                        buffer,
                        "offset = serializeFieldHeader(dataView, offset, {}n, \
                            payloadSize, {});",
                        field.index,
                        integer_encoded(&field.r#type),
                    )?;
                    write_serialization_invocation(
                        buffer,
                        conditional_indentation,
                        &imports,
                        namespace,
                        &field.r#type.variant,
                        true,
                    )?;
                    match field.rule {
                        schema::Rule::Asymmetric | schema::Rule::Required => {}
                        schema::Rule::Optional => {
                            write_indentation(buffer, indentation + 3)?;
                            writeln!(buffer, "}}")?;
                        }
                    }
                    write_indentation(buffer, indentation + 2)?;
                    writeln!(buffer, "}}")?;
                    writeln!(buffer)?;
                }
                write_indentation(buffer, indentation + 2)?;
                writeln!(buffer, "return offset;")?;
                write_indentation(buffer, indentation + 1)?;
                writeln!(buffer, "}}")?;

                writeln!(buffer)?;

                // This function is "unsafe" in the sense that it will throw an error to signify
                // a deserialization failure.
                write_indentation(buffer, indentation + 1)?;
                write!(
                    buffer,
                    "export function deserializeUnsafe(dataView: DataView): ",
                )?;
                write_identifier(buffer, &declaration.name, Pascal, Some(In))?;
                writeln!(buffer, " {{")?;
                write_indentation(buffer, indentation + 2)?;
                writeln!(buffer, "const dataViewAlias = dataView;")?;
                writeln!(buffer)?;
                write_indentation(buffer, indentation + 2)?;
                writeln!(buffer, "let offset = 0;")?;
                writeln!(buffer)?;
                if !declaration.fields.is_empty() {
                    write_indentation(buffer, indentation + 2)?;
                    write!(buffer, "let ")?;
                    let mut first = true;
                    for field in &declaration.fields {
                        if first {
                            first = false;
                        } else {
                            write!(buffer, ", ")?;
                        }
                        write!(buffer, "$")?;
                        write_identifier(buffer, &field.name, Camel, None)?;
                    }
                    writeln!(buffer, ";")?;
                    writeln!(buffer)?;
                }
                write_indentation(buffer, indentation + 2)?;
                writeln!(buffer, "while (true) {{")?;
                write_indentation(buffer, indentation + 3)?;
                writeln!(buffer, "let index, payloadSize;")?;
                writeln!(buffer)?;
                write_indentation(buffer, indentation + 3)?;
                writeln!(buffer, "try {{")?;
                write_indentation(buffer, indentation + 4)?;
                writeln!(
                    buffer,
                    "[offset, index, payloadSize] = \
                        deserializeFieldHeader(dataViewAlias, offset);",
                )?;
                write_indentation(buffer, indentation + 3)?;
                writeln!(buffer, "}} catch (e) {{")?;
                write_indentation(buffer, indentation + 4)?;
                writeln!(buffer, "if (e instanceof RangeError) {{")?;
                write_indentation(buffer, indentation + 5)?;
                writeln!(buffer, "break;")?;
                write_indentation(buffer, indentation + 4)?;
                writeln!(buffer, "}} else {{")?;
                write_indentation(buffer, indentation + 5)?;
                writeln!(buffer, "throw e;")?;
                write_indentation(buffer, indentation + 4)?;
                writeln!(buffer, "}}")?;
                write_indentation(buffer, indentation + 3)?;
                writeln!(buffer, "}}")?;
                writeln!(buffer)?;
                write_indentation(buffer, indentation + 3)?;
                writeln!(buffer, "switch (index) {{")?;
                for field in &declaration.fields {
                    write_indentation(buffer, indentation + 4)?;
                    writeln!(buffer, "case {}n: {{", field.index)?;
                    write_indentation(buffer, indentation + 5)?;
                    writeln!(buffer, "const dataView = new DataView(")?;
                    write_indentation(buffer, indentation + 6)?;
                    writeln!(buffer, "dataViewAlias.buffer,")?;
                    write_indentation(buffer, indentation + 6)?;
                    writeln!(buffer, "dataViewAlias.byteOffset + offset,")?;
                    write_indentation(buffer, indentation + 6)?;
                    writeln!(buffer, "payloadSize,")?;
                    write_indentation(buffer, indentation + 5)?;
                    writeln!(buffer, ");")?;
                    write_indentation(buffer, indentation + 5)?;
                    writeln!(buffer, "const oldOffset = offset;")?;
                    write_indentation(buffer, indentation + 5)?;
                    writeln!(buffer, "offset = 0;")?;
                    write_deserialization_invocation(
                        buffer,
                        indentation + 5,
                        &imports,
                        namespace,
                        &field.r#type.variant,
                        true,
                    )?;
                    write_indentation(buffer, indentation + 5)?;
                    writeln!(buffer, "offset += oldOffset;")?;
                    write_indentation(buffer, indentation + 5)?;
                    write!(buffer, "$")?;
                    write_identifier(buffer, &field.name, Camel, None)?;
                    writeln!(buffer, " = payload;")?;
                    write_indentation(buffer, indentation + 5)?;
                    writeln!(buffer, "break;")?;
                    write_indentation(buffer, indentation + 4)?;
                    writeln!(buffer, "}}")?;
                }
                write_indentation(buffer, indentation + 4)?;
                writeln!(buffer, "default:")?;
                write_indentation(buffer, indentation + 5)?;
                writeln!(buffer, "offset += payloadSize;")?;
                write_indentation(buffer, indentation + 5)?;
                writeln!(buffer, "break;")?;
                write_indentation(buffer, indentation + 3)?;
                writeln!(buffer, "}}")?;
                write_indentation(buffer, indentation + 2)?;
                writeln!(buffer, "}}")?;
                writeln!(buffer)?;
                if declaration.fields.iter().any(|field| match field.rule {
                    schema::Rule::Asymmetric | schema::Rule::Optional => false,
                    schema::Rule::Required => true,
                }) {
                    write_indentation(buffer, indentation + 2)?;
                    write!(buffer, "if (")?;
                    let mut first = true;
                    for field in &declaration.fields {
                        match field.rule {
                            schema::Rule::Asymmetric | schema::Rule::Optional => {}
                            schema::Rule::Required => {
                                if first {
                                    first = false;
                                } else {
                                    writeln!(buffer)?;
                                    write_indentation(buffer, indentation + 3)?;
                                    write!(buffer, "|| ")?;
                                }
                                write!(buffer, "$")?;
                                write_identifier(buffer, &field.name, Camel, None)?;
                                write!(buffer, " === undefined")?;
                            }
                        }
                    }
                    writeln!(buffer, ") {{")?;
                    write_indentation(buffer, indentation + 3)?;
                    writeln!(buffer, "throw new Error(missingFieldsErrorMessage);")?;
                    write_indentation(buffer, indentation + 2)?;
                    writeln!(buffer, "}}")?;
                    writeln!(buffer)?;
                }
                write_indentation(buffer, indentation + 2)?;
                writeln!(buffer, "return {{")?;
                for field in &declaration.fields {
                    write_indentation(buffer, indentation + 3)?;
                    write_identifier(buffer, &field.name, Camel, None)?;
                    write!(buffer, ": $")?;
                    write_identifier(buffer, &field.name, Camel, None)?;
                    writeln!(buffer, ",")?;
                }
                write_indentation(buffer, indentation + 2)?;
                writeln!(buffer, "}};")?;
                write_indentation(buffer, indentation + 1)?;
                writeln!(buffer, "}}")?;

                write_indentation(buffer, indentation)?;
                writeln!(buffer, "}}")?;
            }
            schema::DeclarationVariant::Choice => {
                write_choice(
                    buffer,
                    indentation,
                    &imports,
                    namespace,
                    &declaration.name,
                    &declaration.fields,
                    Atlas,
                )?;

                writeln!(buffer)?;

                write_choice(
                    buffer,
                    indentation,
                    &imports,
                    namespace,
                    &declaration.name,
                    &declaration.fields,
                    Out,
                )?;

                writeln!(buffer)?;

                write_choice(
                    buffer,
                    indentation,
                    &imports,
                    namespace,
                    &declaration.name,
                    &declaration.fields,
                    In,
                )?;

                writeln!(buffer)?;

                write_indentation(buffer, indentation)?;
                write!(buffer, "export namespace ")?;
                write_identifier(buffer, &declaration.name, Pascal, None)?;
                writeln!(buffer, " {{")?;

                write_size_function(
                    buffer,
                    indentation + 1,
                    &declaration.name,
                    &declaration.fields,
                )?;

                writeln!(buffer)?;

                write_serialize_function(
                    buffer,
                    indentation + 1,
                    &declaration.variant,
                    &declaration.name,
                    &declaration.fields,
                )?;

                writeln!(buffer)?;

                write_deserialize_function(buffer, indentation + 1, &declaration.name)?;

                writeln!(buffer)?;

                write_indentation(buffer, indentation + 1)?;
                write!(buffer, "export function atlas(message: ")?;
                write_identifier(buffer, &declaration.name, Pascal, Some(Out))?;
                write!(buffer, "): ")?;
                write_identifier(buffer, &declaration.name, Pascal, Some(Atlas))?;
                writeln!(buffer, " {{")?;
                if declaration.fields.is_empty() {
                    write_indentation(buffer, indentation + 2)?;
                    writeln!(buffer, "return unreachable(message);")?;
                } else {
                    for field in &declaration.fields {
                        write_indentation(buffer, indentation + 2)?;
                        write!(buffer, "if ('")?;
                        write_identifier(buffer, &field.name, Camel, None)?;
                        writeln!(buffer, "' in message) {{")?;
                        write_indentation(buffer, indentation + 3)?;
                        writeln!(buffer, "let payloadAtlas;")?;
                        write_indentation(buffer, indentation + 3)?;
                        write!(buffer, "const payload = message.")?;
                        write_identifier(buffer, &field.name, Camel, None)?;
                        writeln!(buffer, ";")?;
                        write_atlas_calculation(
                            buffer,
                            indentation + 3,
                            &imports,
                            namespace,
                            &field.r#type.variant,
                            true,
                        )?;
                        write_indentation(buffer, indentation + 3)?;
                        write!(buffer, "const payloadSize = ")?;
                        write_atlas_lookup(buffer, &field.r#type.variant)?;
                        writeln!(buffer, ";")?;
                        write_indentation(buffer, indentation + 3)?;
                        match field.rule {
                            schema::Rule::Asymmetric | schema::Rule::Optional => {
                                writeln!(
                                    buffer,
                                    "const fallbackAtlas = atlas(message.$fallback);",
                                )?;
                                write_indentation(buffer, indentation + 3)?;
                                write!(
                                    buffer,
                                    "return {{ $size: fieldHeaderSize({}n, payloadSize, {}) + \
                                        payloadSize + fallbackAtlas.$size, ",
                                    field.index,
                                    integer_encoded(&field.r#type),
                                )?;
                            }
                            schema::Rule::Required => {
                                write!(
                                    buffer,
                                    "return {{ $size: fieldHeaderSize({}n, payloadSize, {}) + \
                                        payloadSize, ",
                                    field.index,
                                    integer_encoded(&field.r#type),
                                )?;
                            }
                        }
                        write_identifier(buffer, &field.name, Camel, None)?;
                        write!(buffer, ": payloadAtlas")?;
                        match field.rule {
                            schema::Rule::Asymmetric | schema::Rule::Optional => {
                                writeln!(buffer, ", $fallback: fallbackAtlas }};")?;
                            }
                            schema::Rule::Required => {
                                writeln!(buffer, " }};")?;
                            }
                        }
                        write_indentation(buffer, indentation + 2)?;
                        writeln!(buffer, "}}")?;
                    }
                    write_indentation(buffer, indentation + 2)?;
                    writeln!(buffer, "return unreachable(message);")?;
                }
                write_indentation(buffer, indentation + 1)?;
                writeln!(buffer, "}}")?;

                writeln!(buffer)?;

                // This function is "unsafe" in the sense that it can throw an error if
                // `dataView` isn't big enough or if `atlas` has incorrect values.
                write_indentation(buffer, indentation + 1)?;
                writeln!(buffer, "export function serializeWithAtlasUnsafe(")?;
                write_indentation(buffer, indentation + 2)?;
                writeln!(buffer, "dataView: DataView,")?;
                write_indentation(buffer, indentation + 2)?;
                writeln!(buffer, "offset: number,")?;
                write_indentation(buffer, indentation + 2)?;
                write!(buffer, "message: ")?;
                write_identifier(buffer, &declaration.name, Pascal, Some(Out))?;
                writeln!(buffer, ",")?;
                write_indentation(buffer, indentation + 2)?;
                write!(buffer, "atlas: ")?;
                write_identifier(buffer, &declaration.name, Pascal, Some(Atlas))?;
                writeln!(buffer, ",")?;
                write_indentation(buffer, indentation + 1)?;
                writeln!(buffer, "): number {{")?;
                if declaration.fields.is_empty() {
                    write_indentation(buffer, indentation + 2)?;
                    writeln!(buffer, "return unreachable(message);")?;
                } else {
                    for field in &declaration.fields {
                        write_indentation(buffer, indentation + 2)?;
                        write!(buffer, "if ('")?;
                        write_identifier(buffer, &field.name, Camel, None)?;
                        writeln!(buffer, "' in message) {{")?;
                        write_indentation(buffer, indentation + 3)?;
                        write!(buffer, "const payload = message.")?;
                        write_identifier(buffer, &field.name, Camel, None)?;
                        writeln!(buffer, ";")?;
                        write_indentation(buffer, indentation + 3)?;
                        write!(buffer, "const payloadAtlas = (atlas as any).")?;
                        write_identifier(buffer, &field.name, Camel, None)?;
                        write!(buffer, " as ")?;
                        write_type(
                            buffer,
                            &imports,
                            namespace,
                            &field.r#type.variant,
                            Direction::Atlas,
                        )?;
                        writeln!(buffer, ";")?;
                        write_indentation(buffer, indentation + 3)?;
                        write!(buffer, "const payloadSize = ")?;
                        write_atlas_lookup(buffer, &field.r#type.variant)?;
                        writeln!(buffer, ";")?;
                        write_indentation(buffer, indentation + 3)?;
                        writeln!(
                            buffer,
                            "offset = serializeFieldHeader(dataView, offset, {}n, \
                                payloadSize, {});",
                            field.index,
                            integer_encoded(&field.r#type),
                        )?;
                        write_serialization_invocation(
                            buffer,
                            indentation + 3,
                            &imports,
                            namespace,
                            &field.r#type.variant,
                            true,
                        )?;
                        match field.rule {
                            schema::Rule::Asymmetric | schema::Rule::Optional => {
                                write_indentation(buffer, indentation + 3)?;
                                write!(
                                    buffer,
                                    "offset = serializeWithAtlasUnsafe(dataView, offset, \
                                        message.$fallback, (atlas as any).$fallback as ",
                                )?;
                                write_identifier(buffer, &declaration.name, Pascal, Some(Atlas))?;
                                writeln!(buffer, ");")?;
                            }
                            schema::Rule::Required => {}
                        }
                        write_indentation(buffer, indentation + 3)?;
                        writeln!(buffer, "return offset;")?;
                        write_indentation(buffer, indentation + 2)?;
                        writeln!(buffer, "}}")?;
                    }
                    write_indentation(buffer, indentation + 2)?;
                    writeln!(buffer, "return unreachable(message);")?;
                }
                write_indentation(buffer, indentation + 1)?;
                writeln!(buffer, "}}")?;

                writeln!(buffer)?;

                // This function is "unsafe" in the sense that it will throw an error to signify
                // a deserialization failure.
                write_indentation(buffer, indentation + 1)?;
                write!(
                    buffer,
                    "export function deserializeUnsafe(dataView: DataView): ",
                )?;
                write_identifier(buffer, &declaration.name, Pascal, Some(In))?;
                writeln!(buffer, " {{")?;
                write_indentation(buffer, indentation + 2)?;
                writeln!(buffer, "const dataViewAlias = dataView;")?;
                writeln!(buffer)?;
                write_indentation(buffer, indentation + 2)?;
                writeln!(buffer, "let offset = 0;")?;
                writeln!(buffer)?;
                write_indentation(buffer, indentation + 2)?;
                writeln!(buffer, "while (true) {{")?;
                write_indentation(buffer, indentation + 3)?;
                writeln!(
                    buffer,
                    "const [newOffset, index, payloadSize] = \
                        deserializeFieldHeader(dataViewAlias, offset);",
                )?;
                write_indentation(buffer, indentation + 3)?;
                writeln!(buffer, "offset = newOffset;")?;
                writeln!(buffer)?;
                write_indentation(buffer, indentation + 3)?;
                writeln!(buffer, "switch (index) {{")?;
                for field in &declaration.fields {
                    write_indentation(buffer, indentation + 4)?;
                    writeln!(buffer, "case {}n: {{", field.index)?;
                    write_indentation(buffer, indentation + 5)?;
                    writeln!(buffer, "const dataView = new DataView(")?;
                    write_indentation(buffer, indentation + 6)?;
                    writeln!(buffer, "dataViewAlias.buffer,")?;
                    write_indentation(buffer, indentation + 6)?;
                    writeln!(buffer, "dataViewAlias.byteOffset + offset,")?;
                    write_indentation(buffer, indentation + 6)?;
                    writeln!(buffer, "payloadSize,")?;
                    write_indentation(buffer, indentation + 5)?;
                    writeln!(buffer, ");")?;
                    write_indentation(buffer, indentation + 5)?;
                    writeln!(buffer, "const oldOffset = offset;")?;
                    write_indentation(buffer, indentation + 5)?;
                    writeln!(buffer, "offset = 0;")?;
                    write_deserialization_invocation(
                        buffer,
                        indentation + 5,
                        &imports,
                        namespace,
                        &field.r#type.variant,
                        true,
                    )?;
                    match field.rule {
                        schema::Rule::Asymmetric | schema::Rule::Required => {}
                        schema::Rule::Optional => {
                            write_indentation(buffer, indentation + 5)?;
                            writeln!(buffer, "offset += oldOffset;")?;
                            write_indentation(buffer, indentation + 5)?;
                            writeln!(buffer, "const $fallback = deserializeUnsafe(")?;
                            write_indentation(buffer, indentation + 6)?;
                            writeln!(buffer, "new DataView(")?;
                            write_indentation(buffer, indentation + 7)?;
                            writeln!(buffer, "dataViewAlias.buffer,")?;
                            write_indentation(buffer, indentation + 7)?;
                            writeln!(buffer, "dataViewAlias.byteOffset + offset,")?;
                            write_indentation(buffer, indentation + 7)?;
                            writeln!(buffer, "dataViewAlias.byteLength - offset,")?;
                            write_indentation(buffer, indentation + 6)?;
                            writeln!(buffer, "),")?;
                            write_indentation(buffer, indentation + 5)?;
                            writeln!(buffer, ");")?;
                        }
                    }
                    write_indentation(buffer, indentation + 5)?;
                    writeln!(buffer, "return {{")?;
                    write_indentation(buffer, indentation + 6)?;
                    write!(buffer, "$field: '")?;
                    write_identifier(buffer, &field.name, Camel, None)?;
                    writeln!(buffer, "',")?;
                    write_indentation(buffer, indentation + 6)?;
                    write_identifier(buffer, &field.name, Camel, None)?;
                    writeln!(buffer, ": payload,")?;
                    match field.rule {
                        schema::Rule::Asymmetric | schema::Rule::Required => {}
                        schema::Rule::Optional => {
                            write_indentation(buffer, indentation + 6)?;
                            writeln!(buffer, "$fallback,")?;
                        }
                    }
                    write_indentation(buffer, indentation + 5)?;
                    writeln!(buffer, "}};")?;
                    write_indentation(buffer, indentation + 4)?;
                    writeln!(buffer, "}}")?;
                }
                write_indentation(buffer, indentation + 4)?;
                writeln!(buffer, "default:")?;
                write_indentation(buffer, indentation + 5)?;
                writeln!(buffer, "offset += payloadSize;")?;
                write_indentation(buffer, indentation + 5)?;
                writeln!(buffer, "break;")?;
                write_indentation(buffer, indentation + 3)?;
                writeln!(buffer, "}}")?;
                write_indentation(buffer, indentation + 2)?;
                writeln!(buffer, "}}")?;
                write_indentation(buffer, indentation + 1)?;
                writeln!(buffer, "}}")?;

                write_indentation(buffer, indentation)?;
                writeln!(buffer, "}}")?;
            }
        }

        if iter.peek().is_some() {
            writeln!(buffer)?;
        }
    }

    Ok(())
}

// Write a struct, including a trailing line break.
fn write_struct<T: Write>(
    buffer: &mut T,
    indentation: usize,
    imports: &BTreeMap<Identifier, schema::Namespace>,
    namespace: &schema::Namespace,
    name: &Identifier,
    fields: &[schema::Field],
    direction: Direction,
) -> Result<(), fmt::Error> {
    write_indentation(buffer, indentation)?;
    write!(buffer, "export type ")?;
    write_identifier(buffer, name, Pascal, Some(direction))?;
    writeln!(buffer, " = {{")?;

    match direction {
        Direction::Atlas => {
            write_indentation(buffer, indentation + 1)?;
            writeln!(buffer, "$size: number;")?;
        }
        Direction::In | Direction::Out => {}
    }

    for field in fields {
        write_indentation(buffer, indentation + 1)?;
        write_identifier(buffer, &field.name, Camel, None)?;
        write!(buffer, ": ")?;
        write_type(buffer, imports, namespace, &field.r#type.variant, direction)?;
        match field.rule {
            schema::Rule::Asymmetric => match direction {
                Direction::Atlas | Direction::Out => {}
                Direction::In => {
                    write!(buffer, " | undefined")?;
                }
            },
            schema::Rule::Optional => {
                write!(buffer, " | undefined")?;
            }
            schema::Rule::Required => {}
        }
        writeln!(buffer, ";")?;
    }

    write_indentation(buffer, indentation)?;
    writeln!(buffer, "}};")?;

    Ok(())
}

// Write a choice, including a trailing line break.
fn write_choice<T: Write>(
    buffer: &mut T,
    indentation: usize,
    imports: &BTreeMap<Identifier, schema::Namespace>,
    namespace: &schema::Namespace,
    name: &Identifier,
    fields: &[schema::Field],
    direction: Direction,
) -> Result<(), fmt::Error> {
    write_indentation(buffer, indentation)?;
    write!(buffer, "export type ")?;
    write_identifier(buffer, name, Pascal, Some(direction))?;
    write!(buffer, " =")?;

    for field in fields {
        writeln!(buffer)?;
        write_indentation(buffer, indentation + 1)?;
        write!(buffer, "| {{ ")?;

        match direction {
            Direction::Atlas => {
                write!(buffer, "$size: number; ")?;
                write_identifier(buffer, &field.name, Camel, None)?;
                write!(buffer, ": ")?;
                write_type(buffer, imports, namespace, &field.r#type.variant, direction)?;
            }
            Direction::In => {
                write!(buffer, "$field: '")?;
                write_identifier(buffer, &field.name, Camel, None)?;
                write!(buffer, "'; ")?;
                write_identifier(buffer, &field.name, Camel, None)?;
                write!(buffer, ": ")?;
                write_type(buffer, imports, namespace, &field.r#type.variant, direction)?;
            }
            Direction::Out => {
                write_identifier(buffer, &field.name, Camel, None)?;
                write!(buffer, ": ")?;
                write_type(buffer, imports, namespace, &field.r#type.variant, direction)?;
            }
        }

        if match field.rule {
            schema::Rule::Asymmetric => match direction {
                Direction::Atlas | Direction::Out => true,
                Direction::In => false,
            },
            schema::Rule::Optional => true,
            schema::Rule::Required => false,
        } {
            write!(buffer, "; $fallback: ")?;
            write_identifier(buffer, name, Pascal, Some(direction))?;
        }

        write!(buffer, " }}")?;
    }

    match direction {
        Direction::Atlas | Direction::Out => {}
        Direction::In => {
            // See https://github.com/microsoft/TypeScript/issues/46978#issuecomment-984093435 for
            // an explanation of this extra case.
            if fields.len() == 1 {
                writeln!(buffer)?;
                write_indentation(buffer, indentation + 1)?;
                write!(buffer, "| {{ $field: never }}")?;
            }
        }
    }

    if fields.is_empty() {
        write!(buffer, " never")?;
    }

    writeln!(buffer, ";")?;

    Ok(())
}

// Write the function to calculate the size of a message.
fn write_size_function<T: Write>(
    buffer: &mut T,
    indentation: usize,
    name: &Identifier,
    fields: &[schema::Field],
) -> Result<(), fmt::Error> {
    write_indentation(buffer, indentation)?;
    write!(buffer, "export function size(message: ")?;
    write_identifier(buffer, name, Pascal, Some(Out))?;
    writeln!(buffer, "): number {{")?;
    write_indentation(buffer, indentation + 1)?;
    if fields.is_empty() {
        writeln!(buffer, "return 0;")?;
    } else {
        writeln!(buffer, "return atlas(message).$size;")?;
    }
    write_indentation(buffer, indentation)?;
    writeln!(buffer, "}}")
}

// Write the function to serialize a message.
fn write_serialize_function<T: Write>(
    buffer: &mut T,
    indentation: usize,
    declaration_variant: &schema::DeclarationVariant,
    name: &Identifier,
    fields: &[schema::Field],
) -> Result<(), fmt::Error> {
    write_indentation(buffer, indentation)?;
    write!(buffer, "export function serialize(message: ")?;
    write_identifier(buffer, name, Pascal, Some(Out))?;
    writeln!(buffer, "): ArrayBuffer {{")?;
    if let (schema::DeclarationVariant::Choice, true) = (declaration_variant, fields.is_empty()) {
        write_indentation(buffer, indentation + 1)?;
        writeln!(buffer, "return unreachable(message);")?;
    } else {
        write_indentation(buffer, indentation + 1)?;
        writeln!(buffer, "const messageAtlas = atlas(message);")?;
        write_indentation(buffer, indentation + 1)?;
        writeln!(
            buffer,
            "const arrayBuffer = new ArrayBuffer(messageAtlas.$size);",
        )?;
        write_indentation(buffer, indentation + 1)?;
        writeln!(buffer, "const dataView = new DataView(arrayBuffer);")?;
        write_indentation(buffer, indentation + 1)?;
        writeln!(
            buffer,
            "serializeWithAtlasUnsafe(dataView, 0, message, messageAtlas);",
        )?;
        write_indentation(buffer, indentation + 1)?;
        writeln!(buffer, "return arrayBuffer;")?;
    }
    write_indentation(buffer, indentation)?;
    writeln!(buffer, "}}")
}

// Write the function to deserialize a message.
fn write_deserialize_function<T: Write>(
    buffer: &mut T,
    indentation: usize,
    name: &Identifier,
) -> Result<(), fmt::Error> {
    write_indentation(buffer, indentation)?;
    write!(
        buffer,
        "export function deserialize(bytes: Deserializable): ",
    )?;
    write_identifier(buffer, name, Pascal, Some(In))?;
    writeln!(buffer, " | Error {{")?;
    write_indentation(buffer, indentation + 1)?;
    writeln!(buffer, "try {{")?;
    write_indentation(buffer, indentation + 2)?;
    writeln!(buffer, "if (bytes instanceof ArrayBuffer) {{")?;
    write_indentation(buffer, indentation + 3)?;
    writeln!(buffer, "return deserializeUnsafe(new DataView(bytes));")?;
    write_indentation(buffer, indentation + 2)?;
    writeln!(buffer, "}}")?;
    write_indentation(buffer, indentation + 2)?;
    writeln!(buffer, "if (bytes instanceof DataView) {{")?;
    write_indentation(buffer, indentation + 3)?;
    writeln!(buffer, "return deserializeUnsafe(bytes);")?;
    write_indentation(buffer, indentation + 2)?;
    writeln!(buffer, "}}")?;
    write_indentation(buffer, indentation + 2)?;
    writeln!(
        buffer,
        "return deserializeUnsafe(new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength));",
    )?;
    write_indentation(buffer, indentation + 1)?;
    writeln!(buffer, "}} catch (e) {{")?;
    write_indentation(buffer, indentation + 2)?;
    writeln!(buffer, "return e as Error;")?;
    write_indentation(buffer, indentation + 1)?;
    writeln!(buffer, "}}")?;
    write_indentation(buffer, indentation)?;
    writeln!(buffer, "}}")
}

// Write a type.
fn write_type<T: Write>(
    buffer: &mut T,
    imports: &BTreeMap<Identifier, schema::Namespace>,
    namespace: &schema::Namespace,
    type_variant: &schema::TypeVariant,
    direction: Direction,
) -> Result<(), fmt::Error> {
    match type_variant {
        schema::TypeVariant::Array(inner_type) => match direction {
            Direction::Atlas => match &inner_type.variant {
                schema::TypeVariant::Array(_)
                | schema::TypeVariant::Bytes
                | schema::TypeVariant::Custom(_, _)
                | schema::TypeVariant::String => {
                    write!(buffer, "{{ $size: number; $elements: ")?;
                    write_type(buffer, imports, namespace, &inner_type.variant, direction)?;
                    write!(buffer, "[] }}")?;
                }
                schema::TypeVariant::Bool
                | schema::TypeVariant::F64
                | schema::TypeVariant::S64
                | schema::TypeVariant::U64
                | schema::TypeVariant::Unit => {
                    write!(buffer, "number")?;
                }
            },
            Direction::In | Direction::Out => {
                write_type(buffer, imports, namespace, &inner_type.variant, direction)?;
                write!(buffer, "[]")?;
            }
        },
        schema::TypeVariant::Bool => match direction {
            Direction::Atlas => {
                write!(buffer, "number")?;
            }
            Direction::In | Direction::Out => {
                write!(buffer, "boolean")?;
            }
        },
        schema::TypeVariant::Bytes => match direction {
            Direction::Atlas => {
                write!(buffer, "number")?;
            }
            Direction::In | Direction::Out => {
                write!(buffer, "ArrayBuffer")?;
            }
        },
        schema::TypeVariant::Custom(import, name) => {
            write_custom_type(
                buffer,
                imports,
                namespace,
                import.as_ref(),
                name,
                Some(direction),
            )?;
        }
        schema::TypeVariant::F64 => {
            write!(buffer, "number")?;
        }
        schema::TypeVariant::S64 | schema::TypeVariant::U64 => match direction {
            Direction::Atlas => {
                write!(buffer, "number")?;
            }
            Direction::In | Direction::Out => {
                write!(buffer, "bigint")?;
            }
        },
        schema::TypeVariant::String => match direction {
            Direction::Atlas => {
                write!(buffer, "Uint8Array")?;
            }
            Direction::In | Direction::Out => {
                write!(buffer, "string")?;
            }
        },
        schema::TypeVariant::Unit => match direction {
            Direction::Atlas => {
                write!(buffer, "number")?;
            }
            Direction::In | Direction::Out => {
                write!(buffer, "null")?;
            }
        },
    }

    Ok(())
}

// Write the fully qualified name of a type.
fn write_custom_type<T: Write>(
    buffer: &mut T,
    imports: &BTreeMap<Identifier, schema::Namespace>,
    namespace: &schema::Namespace,
    import: Option<&Identifier>,
    name: &Identifier,
    direction: Option<Direction>,
) -> Result<(), fmt::Error> {
    let type_namespace = schema::Namespace {
        components: import.as_ref().map_or_else(
            || namespace.components.clone(),
            |import| imports[import].components.clone(),
        ),
    };

    for component in type_namespace.components {
        write_identifier(buffer, &component, Pascal, None)?;
        write!(buffer, ".")?;
    }

    write_identifier(buffer, name, Pascal, direction)
}

// Write an identifier with an optional direction suffix in a way that Rust will be happy with.
fn write_identifier<T: Write>(
    buffer: &mut T,
    identifier: &Identifier,
    case: CaseConvention,
    suffix: Option<Direction>,
) -> Result<(), fmt::Error> {
    let identifier_with_suffix = suffix.map_or_else(
        || identifier.clone(),
        |suffix| {
            identifier.join(
                &match suffix {
                    Direction::Atlas => "Atlas",
                    Direction::In => "In",
                    Direction::Out => "Out",
                }
                .into(),
            )
        },
    );

    let converted_identifier = match case {
        CaseConvention::Camel => identifier_with_suffix.camel_case(),
        CaseConvention::Pascal => identifier_with_suffix.pascal_case(),
    };

    if TYPESCRIPT_KEYWORDS
        .iter()
        .any(|keyword| converted_identifier == *keyword)
    {
        write!(buffer, "_")?;
    }

    write!(buffer, "{converted_identifier}")?;

    Ok(())
}

// Write the given level of indentation.
fn write_indentation<T: Write>(buffer: &mut T, indentation: usize) -> Result<(), fmt::Error> {
    for _ in 0..indentation {
        write!(buffer, "{INDENTATION}")?;
    }

    Ok(())
}

// Write the logic to compute the encoded size of a value.
//
// Context variables:
// - `payloadAtlas` (out)
// - `payload` (in)
#[allow(clippy::too_many_lines)]
fn write_atlas_calculation<T: Write>(
    buffer: &mut T,
    indentation: usize,
    imports: &BTreeMap<Identifier, schema::Namespace>,
    namespace: &schema::Namespace,
    type_variant: &schema::TypeVariant,
    is_field: bool,
) -> Result<(), fmt::Error> {
    match type_variant {
        schema::TypeVariant::Array(inner_type) => match &inner_type.variant {
            schema::TypeVariant::Array(_)
            | schema::TypeVariant::Bytes
            | schema::TypeVariant::Custom(_, _)
            | schema::TypeVariant::String => {
                write_indentation(buffer, indentation)?;
                writeln!(buffer, "{{")?;
                write_indentation(buffer, indentation + 1)?;
                writeln!(buffer, "let size = 0;")?;
                write_indentation(buffer, indentation + 1)?;
                writeln!(buffer, "let elements = [];")?;
                write_indentation(buffer, indentation + 1)?;
                writeln!(buffer, "const oldPayload = payload;")?;
                write_indentation(buffer, indentation + 1)?;
                writeln!(buffer, "for (let i = 0; i < oldPayload.length; i += 1) {{")?;
                write_indentation(buffer, indentation + 2)?;
                writeln!(buffer, "const payload = oldPayload[i];")?;
                write_indentation(buffer, indentation + 2)?;
                writeln!(buffer, "let payloadAtlas;")?;
                write_atlas_calculation(
                    buffer,
                    indentation + 2,
                    imports,
                    namespace,
                    &inner_type.variant,
                    false,
                )?;
                write_indentation(buffer, indentation + 2)?;
                writeln!(buffer, "elements.push(payloadAtlas);")?;
                write_indentation(buffer, indentation + 2)?;
                write!(buffer, "const payloadSize = ")?;
                write_atlas_lookup(buffer, &inner_type.variant)?;
                writeln!(buffer, ";")?;
                write_indentation(buffer, indentation + 2)?;
                writeln!(
                    buffer,
                    "size += varintSizeFromValue(BigInt(payloadSize)) + payloadSize;",
                )?;
                write_indentation(buffer, indentation + 1)?;
                writeln!(buffer, "}}")?;
                write_indentation(buffer, indentation + 1)?;
                writeln!(
                    buffer,
                    "payloadAtlas = {{ $size: size, $elements: elements }};",
                )?;
                write_indentation(buffer, indentation)?;
                writeln!(buffer, "}}")
            }
            schema::TypeVariant::Bool | schema::TypeVariant::S64 | schema::TypeVariant::U64 => {
                write_indentation(buffer, indentation)?;
                writeln!(buffer, "{{")?;
                write_indentation(buffer, indentation + 1)?;
                writeln!(buffer, "let arraySize = 0;")?;
                write_indentation(buffer, indentation + 1)?;
                writeln!(buffer, "const oldPayload = payload;")?;
                write_indentation(buffer, indentation + 1)?;
                writeln!(buffer, "for (let i = 0; i < oldPayload.length; i += 1) {{")?;
                write_indentation(buffer, indentation + 2)?;
                writeln!(buffer, "const payload = oldPayload[i];")?;
                write_indentation(buffer, indentation + 2)?;
                writeln!(buffer, "let payloadAtlas = 0;")?;
                write_atlas_calculation(
                    buffer,
                    indentation + 2,
                    imports,
                    namespace,
                    &inner_type.variant,
                    false,
                )?;
                write_indentation(buffer, indentation + 2)?;
                writeln!(buffer, "arraySize += payloadAtlas;")?;
                write_indentation(buffer, indentation + 1)?;
                writeln!(buffer, "}}")?;
                write_indentation(buffer, indentation + 1)?;
                writeln!(buffer, "payloadAtlas = arraySize;")?;
                write_indentation(buffer, indentation)?;
                writeln!(buffer, "}}")
            }
            schema::TypeVariant::F64 => {
                write_indentation(buffer, indentation)?;
                writeln!(buffer, "payloadAtlas = 8 * payload.length;")
            }
            schema::TypeVariant::Unit => {
                write_indentation(buffer, indentation)?;
                writeln!(buffer, "{{")?;
                write_indentation(buffer, indentation + 1)?;
                writeln!(buffer, "const oldPayload = payload;")?;
                write_indentation(buffer, indentation + 1)?;
                writeln!(buffer, "{{")?;
                write_indentation(buffer, indentation + 2)?;
                writeln!(buffer, "const payload = BigInt(oldPayload.length);")?;
                write_atlas_calculation(
                    buffer,
                    indentation + 2,
                    imports,
                    namespace,
                    &schema::TypeVariant::U64,
                    is_field,
                )?;
                write_indentation(buffer, indentation + 1)?;
                writeln!(buffer, "}}")?;
                write_indentation(buffer, indentation)?;
                writeln!(buffer, "}}")
            }
        },
        schema::TypeVariant::Bool => {
            write_indentation(buffer, indentation)?;
            if is_field {
                writeln!(buffer, "if (payload) {{")?;
                write_indentation(buffer, indentation + 1)?;
                writeln!(buffer, "payloadAtlas = 1;")?;
                write_indentation(buffer, indentation)?;
                writeln!(buffer, "}} else {{")?;
                write_indentation(buffer, indentation + 1)?;
                writeln!(buffer, "payloadAtlas = 0;")?;
                write_indentation(buffer, indentation)?;
                writeln!(buffer, "}}")
            } else {
                writeln!(buffer, "payloadAtlas = 1;")
            }
        }
        schema::TypeVariant::Bytes => {
            write_indentation(buffer, indentation)?;
            writeln!(buffer, "payloadAtlas = payload.byteLength;")
        }
        schema::TypeVariant::String => {
            write_indentation(buffer, indentation)?;
            writeln!(buffer, "payloadAtlas = textEncoder.encode(payload);")
        }
        schema::TypeVariant::Custom(import, name) => {
            write_indentation(buffer, indentation)?;
            write!(buffer, "payloadAtlas = ")?;
            write_custom_type(buffer, imports, namespace, import.as_ref(), name, None)?;
            writeln!(buffer, ".atlas(payload);")
        }
        schema::TypeVariant::F64 => {
            write_indentation(buffer, indentation)?;
            if is_field {
                writeln!(buffer, "if (Object.is(payload, 0)) {{")?;
                write_indentation(buffer, indentation + 1)?;
                writeln!(buffer, "payloadAtlas = 0;")?;
                write_indentation(buffer, indentation)?;
                writeln!(buffer, "}} else {{")?;
                write_indentation(buffer, indentation + 1)?;
                writeln!(buffer, "payloadAtlas = 8;")?;
                write_indentation(buffer, indentation)?;
                writeln!(buffer, "}}")
            } else {
                writeln!(buffer, "payloadAtlas = 8;")
            }
        }
        schema::TypeVariant::S64 => {
            write_indentation(buffer, indentation)?;
            if is_field {
                writeln!(buffer, "{{")?;
                write_indentation(buffer, indentation + 1)?;
                writeln!(buffer, "const zigzag = zigzagEncode(payload);")?;
                write_indentation(buffer, indentation + 1)?;
                writeln!(buffer, "if (zigzag === 0n) {{")?;
                write_indentation(buffer, indentation + 2)?;
                writeln!(buffer, "payloadAtlas = 0;")?;
                write_indentation(buffer, indentation + 1)?;
                writeln!(buffer, "}} else if (zigzag < 567_382_630_219_904n) {{")?;
                write_indentation(buffer, indentation + 2)?;
                writeln!(buffer, "payloadAtlas = varintSizeFromValue(zigzag);")?;
                write_indentation(buffer, indentation + 1)?;
                writeln!(buffer, "}} else {{")?;
                write_indentation(buffer, indentation + 2)?;
                writeln!(buffer, "payloadAtlas = 8;")?;
                write_indentation(buffer, indentation + 1)?;
                writeln!(buffer, "}}")?;
                write_indentation(buffer, indentation)?;
                writeln!(buffer, "}}")
            } else {
                writeln!(
                    buffer,
                    "payloadAtlas = varintSizeFromValue(zigzagEncode(payload));",
                )
            }
        }
        schema::TypeVariant::U64 => {
            write_indentation(buffer, indentation)?;
            if is_field {
                writeln!(buffer, "if (payload === 0n) {{")?;
                write_indentation(buffer, indentation + 1)?;
                writeln!(buffer, "payloadAtlas = 0;")?;
                write_indentation(buffer, indentation)?;
                writeln!(buffer, "}} else if (payload < 567_382_630_219_904n) {{")?;
                write_indentation(buffer, indentation + 1)?;
                writeln!(buffer, "payloadAtlas = varintSizeFromValue(payload);")?;
                write_indentation(buffer, indentation)?;
                writeln!(buffer, "}} else {{")?;
                write_indentation(buffer, indentation + 1)?;
                writeln!(buffer, "payloadAtlas = 8;")?;
                write_indentation(buffer, indentation)?;
                writeln!(buffer, "}}")
            } else {
                writeln!(buffer, "payloadAtlas = varintSizeFromValue(payload);")
            }
        }
        schema::TypeVariant::Unit => {
            write_indentation(buffer, indentation)?;
            writeln!(buffer, "payloadAtlas = 0;")
        }
    }
}

// Write the logic to look up the encoded size of a value from its atlas.
//
// Context variables:
// - `payloadAtlas` (in)
fn write_atlas_lookup<T: Write>(
    buffer: &mut T,
    type_variant: &schema::TypeVariant,
) -> Result<(), fmt::Error> {
    match type_variant {
        schema::TypeVariant::Array(inner_type) => match &inner_type.variant {
            schema::TypeVariant::Array(_)
            | schema::TypeVariant::Bytes
            | schema::TypeVariant::Custom(_, _)
            | schema::TypeVariant::String => write!(buffer, "payloadAtlas.$size"),
            schema::TypeVariant::Bool
            | schema::TypeVariant::F64
            | schema::TypeVariant::S64
            | schema::TypeVariant::U64
            | schema::TypeVariant::Unit => write!(buffer, "payloadAtlas"),
        },
        schema::TypeVariant::Bool
        | schema::TypeVariant::Bytes
        | schema::TypeVariant::F64
        | schema::TypeVariant::S64
        | schema::TypeVariant::U64
        | schema::TypeVariant::Unit => write!(buffer, "payloadAtlas"),
        schema::TypeVariant::Custom(_, _) => {
            // The type assertion is needed for singleton choices and empty choices, which are
            // special cases due to the nature of TypeScript's type system.
            write!(buffer, "(payloadAtlas as {{ $size: number }}).$size")
        }
        schema::TypeVariant::String => write!(buffer, "payloadAtlas.byteLength"),
    }
}

// Write the logic to invoke the serialization logic for a value, including a trailing line break.
//
// Context variables:
// - `dataView` (in and out)
// - `offset` (in and out)
// - `payloadAtlas` (in)
// - `payload` (in)
//
// Additional notes:
// - If `is_field` is unset and `type_variant` is `Bool`, `S64`, `U64`, or `F64`, then
//   `payloadAtlas` is never read.
#[allow(clippy::too_many_lines)]
fn write_serialization_invocation<T: Write>(
    buffer: &mut T,
    indentation: usize,
    imports: &BTreeMap<Identifier, schema::Namespace>,
    namespace: &schema::Namespace,
    type_variant: &schema::TypeVariant,
    is_field: bool,
) -> Result<(), fmt::Error> {
    match type_variant {
        schema::TypeVariant::Array(inner_type) => match &inner_type.variant {
            schema::TypeVariant::Array(_)
            | schema::TypeVariant::Bytes
            | schema::TypeVariant::Custom(_, _)
            | schema::TypeVariant::String => {
                write_indentation(buffer, indentation)?;
                writeln!(buffer, "{{")?;
                write_indentation(buffer, indentation + 1)?;
                writeln!(buffer, "const oldPayload = payload;")?;
                write_indentation(buffer, indentation + 1)?;
                writeln!(buffer, "const oldPayloadAtlas = payloadAtlas;")?;
                write_indentation(buffer, indentation + 1)?;
                writeln!(buffer, "for (let i = 0; i < oldPayload.length; i += 1) {{")?;
                write_indentation(buffer, indentation + 2)?;
                writeln!(buffer, "const payload = oldPayload[i];")?;
                write_indentation(buffer, indentation + 2)?;
                writeln!(buffer, "const payloadAtlas = oldPayloadAtlas.$elements[i];")?;
                write_indentation(buffer, indentation + 2)?;
                write!(buffer, "offset = serializeVarint(dataView, offset, BigInt(")?;
                write_atlas_lookup(buffer, &inner_type.variant)?;
                writeln!(buffer, "));")?;
                write_serialization_invocation(
                    buffer,
                    indentation + 2,
                    imports,
                    namespace,
                    &inner_type.variant,
                    false,
                )?;
                write_indentation(buffer, indentation + 1)?;
                writeln!(buffer, "}}")?;
                write_indentation(buffer, indentation)?;
                writeln!(buffer, "}}")
            }
            schema::TypeVariant::Bool
            | schema::TypeVariant::S64
            | schema::TypeVariant::U64
            | schema::TypeVariant::F64 => {
                write_indentation(buffer, indentation)?;
                writeln!(buffer, "{{")?;
                write_indentation(buffer, indentation + 1)?;
                writeln!(buffer, "const oldPayload = payload;")?;
                write_indentation(buffer, indentation + 1)?;
                writeln!(buffer, "for (let i = 0; i < oldPayload.length; i += 1) {{")?;
                write_indentation(buffer, indentation + 2)?;
                writeln!(buffer, "const payload = oldPayload[i];")?;
                write_serialization_invocation(
                    buffer,
                    indentation + 2,
                    imports,
                    namespace,
                    &inner_type.variant,
                    false,
                )?;
                write_indentation(buffer, indentation + 1)?;
                writeln!(buffer, "}}")?;
                write_indentation(buffer, indentation)?;
                writeln!(buffer, "}}")
            }
            schema::TypeVariant::Unit => {
                write_indentation(buffer, indentation)?;
                writeln!(buffer, "{{")?;
                write_indentation(buffer, indentation + 1)?;
                writeln!(buffer, "const varint = BigInt(payload.length);")?;
                write_u64_serialization_invocation(buffer, indentation + 1, is_field)?;
                write_indentation(buffer, indentation)?;
                writeln!(buffer, "}}")
            }
        },
        schema::TypeVariant::Bool => {
            write_indentation(buffer, indentation)?;
            writeln!(buffer, "{{")?;
            write_indentation(buffer, indentation + 1)?;
            writeln!(buffer, "const varint = payload ? 1n : 0n;")?;
            write_u64_serialization_invocation(buffer, indentation + 1, is_field)?;
            write_indentation(buffer, indentation)?;
            writeln!(buffer, "}}")
        }
        schema::TypeVariant::Bytes => {
            write_indentation(buffer, indentation)?;
            writeln!(buffer, "{{")?;
            write_indentation(buffer, indentation + 1)?;
            writeln!(buffer, "const sourceBuffer = new Uint8Array(payload);")?;
            write_indentation(buffer, indentation + 1)?;
            writeln!(buffer, "const targetBuffer = new Uint8Array(")?;
            write_indentation(buffer, indentation + 2)?;
            writeln!(buffer, "dataView.buffer,")?;
            write_indentation(buffer, indentation + 2)?;
            writeln!(buffer, "dataView.byteOffset,")?;
            write_indentation(buffer, indentation + 2)?;
            writeln!(buffer, "dataView.byteLength,")?;
            write_indentation(buffer, indentation + 1)?;
            writeln!(buffer, ");")?;
            write_indentation(buffer, indentation + 1)?;
            writeln!(buffer, "targetBuffer.set(sourceBuffer, offset);")?;
            write_indentation(buffer, indentation + 1)?;
            writeln!(buffer, "offset += sourceBuffer.byteLength;")?;
            write_indentation(buffer, indentation)?;
            writeln!(buffer, "}}")
        }
        schema::TypeVariant::Custom(import, name) => {
            write_indentation(buffer, indentation)?;
            write!(buffer, "offset = ")?;
            write_custom_type(buffer, imports, namespace, import.as_ref(), name, None)?;
            writeln!(
                buffer,
                ".serializeWithAtlasUnsafe(dataView, offset, payload, payloadAtlas);",
            )
        }
        schema::TypeVariant::F64 => {
            write_indentation(buffer, indentation)?;
            if is_field {
                write!(buffer, "if (")?;
                write_atlas_lookup(buffer, type_variant)?;
                writeln!(buffer, " !== 0) {{")?;
                write_indentation(buffer, indentation + 1)?;
                writeln!(buffer, "dataView.setFloat64(offset, payload, true);")?;
                write_indentation(buffer, indentation + 1)?;
                writeln!(buffer, "offset += 8;")?;
                write_indentation(buffer, indentation)?;
                writeln!(buffer, "}}")
            } else {
                writeln!(buffer, "dataView.setFloat64(offset, payload, true);")?;
                write_indentation(buffer, indentation)?;
                writeln!(buffer, "offset += 8;")
            }
        }
        schema::TypeVariant::S64 => {
            write_indentation(buffer, indentation)?;
            writeln!(buffer, "{{")?;
            write_indentation(buffer, indentation + 1)?;
            writeln!(buffer, "const varint = zigzagEncode(payload);")?;
            write_u64_serialization_invocation(buffer, indentation + 1, is_field)?;
            write_indentation(buffer, indentation)?;
            writeln!(buffer, "}}")
        }
        schema::TypeVariant::String => {
            write_indentation(buffer, indentation)?;
            writeln!(buffer, "{{")?;
            write_indentation(buffer, indentation + 1)?;
            writeln!(buffer, "const targetBuffer = new Uint8Array(")?;
            write_indentation(buffer, indentation + 2)?;
            writeln!(buffer, "dataView.buffer,")?;
            write_indentation(buffer, indentation + 2)?;
            writeln!(buffer, "dataView.byteOffset,")?;
            write_indentation(buffer, indentation + 2)?;
            writeln!(buffer, "dataView.byteLength,")?;
            write_indentation(buffer, indentation + 1)?;
            writeln!(buffer, ");")?;
            write_indentation(buffer, indentation + 1)?;
            writeln!(buffer, "targetBuffer.set(payloadAtlas, offset);")?;
            write_indentation(buffer, indentation + 1)?;
            writeln!(buffer, "offset += payloadAtlas.byteLength;")?;
            write_indentation(buffer, indentation)?;
            writeln!(buffer, "}}")
        }
        schema::TypeVariant::U64 => {
            write_indentation(buffer, indentation)?;
            writeln!(buffer, "{{")?;
            write_indentation(buffer, indentation + 1)?;
            writeln!(buffer, "const varint = payload;")?;
            write_u64_serialization_invocation(buffer, indentation + 1, is_field)?;
            write_indentation(buffer, indentation)?;
            writeln!(buffer, "}}")
        }
        schema::TypeVariant::Unit => Ok(()),
    }
}

// Write the logic to invoke the serialization logic for a varint, including a trailing line break.
//
// Context variables:
// - `dataView` (in and out)
// - `offset` (in and out)
// - `varint` (in)
fn write_u64_serialization_invocation<T: Write>(
    buffer: &mut T,
    indentation: usize,
    is_field: bool,
) -> Result<(), fmt::Error> {
    write_indentation(buffer, indentation)?;
    if is_field {
        writeln!(buffer, "if (varint > 567_382_630_219_903n) {{")?;
        write_indentation(buffer, indentation + 1)?;
        writeln!(buffer, "dataView.setBigUint64(offset, varint, true);")?;
        write_indentation(buffer, indentation + 1)?;
        writeln!(buffer, "offset += 8;")?;
        write_indentation(buffer, indentation)?;
        writeln!(buffer, "}} else if (varint !== 0n) {{")?;
        write_indentation(buffer, indentation + 1)?;
        writeln!(
            buffer,
            "offset = serializeVarint(dataView, offset, varint);",
        )?;
        write_indentation(buffer, indentation)?;
        writeln!(buffer, "}}")
    } else {
        writeln!(
            buffer,
            "offset = serializeVarint(dataView, offset, varint);",
        )
    }
}

// Write the logic to invoke the deserialization logic for a value, including a trailing line break.
//
// Context variables:
// - `dataView` (in and out)
// - `offset` (in and out)
// - `payloadSize` (in, unused if `is_field` is `false`)
// - `payload` (out, introduced)
//
// Additional notes:
// - This function introduces the `payload` variable with `let` rather than `const`, since it relies
//   on being able to mutate the `payload` from a recursive call.
// - If `type_variant` is `Array`, `Bytes`, `Custom`, or `String` and the encoded data is well-
//   formed, then `dataView` is consumed to the end.
// - If `type_variant` is `Custom`, then `offset` must be 0.
#[allow(clippy::too_many_lines)]
fn write_deserialization_invocation<T: Write>(
    buffer: &mut T,
    indentation: usize,
    imports: &BTreeMap<Identifier, schema::Namespace>,
    namespace: &schema::Namespace,
    type_variant: &schema::TypeVariant,
    is_field: bool,
) -> Result<(), fmt::Error> {
    match type_variant {
        schema::TypeVariant::Array(inner_type) => match &inner_type.variant {
            schema::TypeVariant::Array(_)
            | schema::TypeVariant::Bytes
            | schema::TypeVariant::Custom(_, _)
            | schema::TypeVariant::String => {
                write_indentation(buffer, indentation)?;
                write!(buffer, "let payload: ")?;
                write_type(buffer, imports, namespace, &inner_type.variant, In)?;
                writeln!(buffer, "[] = [];")?;
                write_indentation(buffer, indentation)?;
                writeln!(buffer, "{{")?;
                write_indentation(buffer, indentation + 1)?;
                writeln!(buffer, "const dataViewAlias = dataView;")?;
                write_indentation(buffer, indentation + 1)?;
                writeln!(buffer, "const payloadAlias = payload;")?;
                write_indentation(buffer, indentation + 1)?;
                writeln!(buffer, "{{")?;
                write_indentation(buffer, indentation + 2)?;
                writeln!(buffer, "while (true) {{")?;
                write_indentation(buffer, indentation + 3)?;
                writeln!(buffer, "let payloadSizeBig;")?;
                write_indentation(buffer, indentation + 3)?;
                writeln!(buffer, "try {{")?;
                write_indentation(buffer, indentation + 4)?;
                writeln!(
                    buffer,
                    "[offset, payloadSizeBig] = deserializeVarint(dataViewAlias, offset);",
                )?;
                write_indentation(buffer, indentation + 3)?;
                writeln!(buffer, "}} catch (e) {{")?;
                write_indentation(buffer, indentation + 4)?;
                writeln!(buffer, "if (e instanceof RangeError) {{")?;
                write_indentation(buffer, indentation + 5)?;
                writeln!(buffer, "break;")?;
                write_indentation(buffer, indentation + 4)?;
                writeln!(buffer, "}} else {{")?;
                write_indentation(buffer, indentation + 5)?;
                writeln!(buffer, "throw e;")?;
                write_indentation(buffer, indentation + 4)?;
                writeln!(buffer, "}}")?;
                write_indentation(buffer, indentation + 3)?;
                writeln!(buffer, "}}")?;
                write_indentation(buffer, indentation + 3)?;
                writeln!(buffer, "const dataView = new DataView(")?;
                write_indentation(buffer, indentation + 4)?;
                writeln!(buffer, "dataViewAlias.buffer,")?;
                write_indentation(buffer, indentation + 4)?;
                writeln!(buffer, "dataViewAlias.byteOffset + offset,")?;
                write_indentation(buffer, indentation + 4)?;
                writeln!(buffer, "Number(payloadSizeBig),")?;
                write_indentation(buffer, indentation + 3)?;
                writeln!(buffer, ");")?;
                write_indentation(buffer, indentation + 3)?;
                writeln!(buffer, "const oldOffset = offset;")?;
                write_indentation(buffer, indentation + 3)?;
                writeln!(buffer, "offset = 0;")?;
                write_deserialization_invocation(
                    buffer,
                    indentation + 3,
                    imports,
                    namespace,
                    &inner_type.variant,
                    false,
                )?;
                write_indentation(buffer, indentation + 3)?;
                writeln!(buffer, "offset += oldOffset;")?;
                write_indentation(buffer, indentation + 3)?;
                writeln!(buffer, "payloadAlias.push(payload);")?;
                write_indentation(buffer, indentation + 2)?;
                writeln!(buffer, "}}")?;
                write_indentation(buffer, indentation + 1)?;
                writeln!(buffer, "}}")?;
                write_indentation(buffer, indentation)?;
                writeln!(buffer, "}}")
            }
            schema::TypeVariant::Bool
            | schema::TypeVariant::S64
            | schema::TypeVariant::U64
            | schema::TypeVariant::F64 => {
                write_indentation(buffer, indentation)?;
                write!(buffer, "let payload: ")?;
                write_type(buffer, imports, namespace, &inner_type.variant, In)?;
                writeln!(buffer, "[] = [];")?;
                write_indentation(buffer, indentation)?;
                writeln!(buffer, "{{")?;
                write_indentation(buffer, indentation + 1)?;
                writeln!(buffer, "const payloadAlias = payload;")?;
                write_indentation(buffer, indentation + 1)?;
                writeln!(buffer, "{{")?;
                write_indentation(buffer, indentation + 2)?;
                writeln!(buffer, "while (true) {{")?;
                write_indentation(buffer, indentation + 3)?;
                writeln!(buffer, "try {{")?;
                write_deserialization_invocation(
                    buffer,
                    indentation + 4,
                    imports,
                    namespace,
                    &inner_type.variant,
                    false,
                )?;
                write_indentation(buffer, indentation + 4)?;
                writeln!(buffer, "payloadAlias.push(payload);")?;
                write_indentation(buffer, indentation + 3)?;
                writeln!(buffer, "}} catch (e) {{")?;
                write_indentation(buffer, indentation + 4)?;
                writeln!(buffer, "if (e instanceof RangeError) {{")?;
                write_indentation(buffer, indentation + 5)?;
                writeln!(buffer, "break;")?;
                write_indentation(buffer, indentation + 4)?;
                writeln!(buffer, "}} else {{")?;
                write_indentation(buffer, indentation + 5)?;
                writeln!(buffer, "throw e;")?;
                write_indentation(buffer, indentation + 4)?;
                writeln!(buffer, "}}")?;
                write_indentation(buffer, indentation + 3)?;
                writeln!(buffer, "}}")?;
                write_indentation(buffer, indentation + 2)?;
                writeln!(buffer, "}}")?;
                write_indentation(buffer, indentation + 1)?;
                writeln!(buffer, "}}")?;
                write_indentation(buffer, indentation)?;
                writeln!(buffer, "}}")
            }
            schema::TypeVariant::Unit => {
                write_indentation(buffer, indentation)?;
                writeln!(buffer, "let payload;")?;
                write_indentation(buffer, indentation)?;
                writeln!(buffer, "{{")?;
                write_indentation(buffer, indentation + 1)?;
                writeln!(buffer, "let newPayload;")?;
                write_indentation(buffer, indentation + 1)?;
                writeln!(buffer, "{{")?;
                write_deserialization_invocation(
                    buffer,
                    indentation + 2,
                    imports,
                    namespace,
                    &schema::TypeVariant::U64,
                    is_field,
                )?;
                write_indentation(buffer, indentation + 2)?;
                writeln!(
                    buffer,
                    "newPayload = Array(Number(payload)).fill(null) as null[];",
                )?;
                write_indentation(buffer, indentation + 1)?;
                writeln!(buffer, "}}")?;
                write_indentation(buffer, indentation + 1)?;
                writeln!(buffer, "payload = newPayload;")?;
                write_indentation(buffer, indentation)?;
                writeln!(buffer, "}}")
            }
        },
        schema::TypeVariant::Bool => {
            write_indentation(buffer, indentation)?;
            writeln!(buffer, "let payload;")?;
            write_indentation(buffer, indentation)?;
            writeln!(buffer, "{{")?;
            write_indentation(buffer, indentation + 1)?;
            writeln!(buffer, "let newPayload;")?;
            write_indentation(buffer, indentation + 1)?;
            writeln!(buffer, "{{")?;
            write_deserialization_invocation(
                buffer,
                indentation + 2,
                imports,
                namespace,
                &schema::TypeVariant::U64,
                is_field,
            )?;
            write_indentation(buffer, indentation + 2)?;
            writeln!(buffer, "newPayload = payload !== 0n;")?;
            write_indentation(buffer, indentation + 1)?;
            writeln!(buffer, "}}")?;
            write_indentation(buffer, indentation + 1)?;
            writeln!(buffer, "payload = newPayload;")?;
            write_indentation(buffer, indentation)?;
            writeln!(buffer, "}}")
        }
        schema::TypeVariant::Bytes => {
            write_indentation(buffer, indentation)?;
            writeln!(buffer, "let payload = dataView.buffer.slice(")?;
            write_indentation(buffer, indentation + 1)?;
            writeln!(buffer, "dataView.byteOffset + offset,")?;
            write_indentation(buffer, indentation + 1)?;
            writeln!(buffer, "dataView.byteOffset + dataView.byteLength,")?;
            write_indentation(buffer, indentation)?;
            writeln!(buffer, ");")?;
            write_indentation(buffer, indentation)?;
            writeln!(buffer, "offset = dataView.byteLength;")
        }
        schema::TypeVariant::Custom(import, name) => {
            write_indentation(buffer, indentation)?;
            write!(buffer, "let payload = ")?;
            write_custom_type(buffer, imports, namespace, import.as_ref(), name, None)?;
            writeln!(buffer, ".deserializeUnsafe(dataView);")?;
            write_indentation(buffer, indentation)?;
            writeln!(buffer, "offset = dataView.byteLength;")
        }
        schema::TypeVariant::F64 => {
            write_indentation(buffer, indentation)?;
            if is_field {
                writeln!(buffer, "let payload;")?;
                write_indentation(buffer, indentation)?;
                writeln!(buffer, "{{")?;
                write_indentation(buffer, indentation + 1)?;
                writeln!(buffer, "switch (payloadSize) {{")?;
                write_indentation(buffer, indentation + 2)?;
                writeln!(buffer, "case 0:")?;
                write_indentation(buffer, indentation + 3)?;
                writeln!(buffer, "payload = 0;")?;
                write_indentation(buffer, indentation + 3)?;
                writeln!(buffer, "break;")?;
                write_indentation(buffer, indentation + 2)?;
                writeln!(buffer, "default:")?;
                write_indentation(buffer, indentation + 3)?;
                writeln!(buffer, "payload = dataView.getFloat64(offset, true);")?;
                write_indentation(buffer, indentation + 3)?;
                writeln!(buffer, "offset += 8;")?;
                write_indentation(buffer, indentation + 3)?;
                writeln!(buffer, "break;")?;
                write_indentation(buffer, indentation + 1)?;
                writeln!(buffer, "}}")?;
                write_indentation(buffer, indentation)?;
                writeln!(buffer, "}}")
            } else {
                writeln!(buffer, "let payload = dataView.getFloat64(offset, true);")?;
                write_indentation(buffer, indentation)?;
                writeln!(buffer, "offset += 8;")
            }
        }
        schema::TypeVariant::S64 => {
            write_deserialization_invocation(
                buffer,
                indentation,
                imports,
                namespace,
                &schema::TypeVariant::U64,
                is_field,
            )?;
            write_indentation(buffer, indentation)?;
            writeln!(buffer, "payload = zigzagDecode(payload);")
        }
        schema::TypeVariant::String => {
            write_indentation(buffer, indentation)?;
            writeln!(buffer, "let payload = textDecoder.decode(")?;
            write_indentation(buffer, indentation + 1)?;
            writeln!(buffer, "new Uint8Array(")?;
            write_indentation(buffer, indentation + 2)?;
            writeln!(buffer, "dataView.buffer,")?;
            write_indentation(buffer, indentation + 2)?;
            writeln!(buffer, "dataView.byteOffset + offset,")?;
            write_indentation(buffer, indentation + 2)?;
            writeln!(buffer, "dataView.byteLength - offset,")?;
            write_indentation(buffer, indentation + 1)?;
            writeln!(buffer, "),")?;
            write_indentation(buffer, indentation)?;
            writeln!(buffer, ");")?;
            write_indentation(buffer, indentation)?;
            writeln!(buffer, "offset = dataView.byteLength;")
        }
        schema::TypeVariant::U64 => {
            write_indentation(buffer, indentation)?;
            writeln!(buffer, "let payload;")?;
            write_indentation(buffer, indentation)?;
            if is_field {
                writeln!(buffer, "{{")?;
                write_indentation(buffer, indentation + 1)?;
                writeln!(buffer, "switch (payloadSize) {{")?;
                write_indentation(buffer, indentation + 2)?;
                writeln!(buffer, "case 0:")?;
                write_indentation(buffer, indentation + 3)?;
                writeln!(buffer, "payload = 0n;")?;
                write_indentation(buffer, indentation + 3)?;
                writeln!(buffer, "break;")?;
                write_indentation(buffer, indentation + 2)?;
                writeln!(buffer, "case 8:")?;
                write_indentation(buffer, indentation + 3)?;
                writeln!(buffer, "payload = dataView.getBigUint64(offset, true);")?;
                write_indentation(buffer, indentation + 3)?;
                writeln!(buffer, "offset += 8;")?;
                write_indentation(buffer, indentation + 3)?;
                writeln!(buffer, "break;")?;
                write_indentation(buffer, indentation + 2)?;
                writeln!(buffer, "default:")?;
                write_indentation(buffer, indentation + 3)?;
                writeln!(
                    buffer,
                    "[offset, payload] = deserializeVarint(dataView, offset);",
                )?;
                write_indentation(buffer, indentation + 3)?;
                writeln!(buffer, "break;")?;
                write_indentation(buffer, indentation + 1)?;
                writeln!(buffer, "}}")?;
                write_indentation(buffer, indentation)?;
                writeln!(buffer, "}}")
            } else {
                writeln!(
                    buffer,
                    "[offset, payload] = deserializeVarint(dataView, offset);",
                )
            }
        }
        schema::TypeVariant::Unit => {
            write_indentation(buffer, indentation)?;
            writeln!(buffer, "let payload = null;")
        }
    }
}

// Determine whether a type is encoded as a varint.
fn integer_encoded(r#type: &schema::Type) -> bool {
    match &r#type.variant {
        schema::TypeVariant::Bool | schema::TypeVariant::S64 | schema::TypeVariant::U64 => true,
        schema::TypeVariant::Array(_)
        | schema::TypeVariant::Bytes
        | schema::TypeVariant::Custom(_, _)
        | schema::TypeVariant::F64
        | schema::TypeVariant::String
        | schema::TypeVariant::Unit => false,
    }
}

#[cfg(test)]
mod tests {
    use {
        crate::{generate_typescript::generate, schema_loader::load_schemas, validator::validate},
        std::{fs::read_to_string, path::Path},
    };

    #[test]
    fn generate_example() {
        let schemas = load_schemas(Path::new("integration_tests/types/types.t")).unwrap();
        validate(&schemas).unwrap();

        assert_eq!(
            generate("0.0.0", &schemas),
            read_to_string("test_data/types.ts").unwrap(),
        );
    }
}
